﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using AnimationPackerEngine.Animation.CommandWriters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SpriteAtlasRuntime;

namespace AnimationPackerEngine.Animation
{
    public class AnimationCompiler
    {
        public struct RawAnimationScript
        {
            public string Name { get; set; }
            internal List<ICommandWriter> Commands { get; set; }
        }

        public bool Ignore { get; private set; }
        public bool SimpleList { get; private set; }
        public List<RawAnimationScript> Animations { get; set; }

        private IPackingStatus mStatus;

        internal AnimationCompiler(string directory, IPackingStatus status)
        {
            mStatus = status;

            Animations = new List<RawAnimationScript>();
            Ignore = false;

            // assume the config file is invalid
            SimpleList = true;

            string file = Path.Combine(directory, "config.json");

            if (File.Exists(file))
            {
                try
                {
                    using (StreamReader fstream = File.OpenText(file))
                    using (JsonReader json = new JsonTextReader(fstream))
                    {
                        JObject scripts = JObject.Load(json);

                        if (scripts["ignore"] != null)
                        {
                            if (scripts["ignore"].Value<bool>())
                            {
                                Ignore = true;
                                mStatus.Information("Ignoring directory [" + directory + "] and all subdirectories.");
                                return;
                            }
                        }

                        foreach (JProperty script in scripts.Properties())
                        {
                            RawAnimationScript animationScript = new RawAnimationScript();
                            animationScript.Name = script.Name;
                            animationScript.Commands = new List<ICommandWriter>();

                            ParseCommandBlock(animationScript.Commands, script.Value as JArray);

                            Animations.Add(animationScript);
                        }
                    }

                    SimpleList = false;
                }
                catch (Exception e)
                {
                    mStatus.FatalException("Error parsing directory [" + directory + "]", e);
                }
            }
        }

        #region Parser
        private void ParseCommandBlock(List<ICommandWriter> commands, JArray scriptCommands)
        {
            foreach (JObject command in scriptCommands)
            {
                ICommandWriter newCommand = null;

                JProperty cmdValue = (JProperty)command.First;

                ScriptCommands cmd;
                if (Enum.TryParse<ScriptCommands>(cmdValue.Name, true, out cmd))
                {
                    switch (cmd)
                    {
                        case ScriptCommands.Set:
                            newCommand = ParseSetCommand(cmdValue.Value as JObject);
                            break;

                        case ScriptCommands.If:
                            newCommand = ParseIfCommand(cmdValue.Value as JObject);
                            break;

                        case ScriptCommands.Label:
                            newCommand = ParseLabel(cmdValue.Value as JValue);
                            break;

                        case ScriptCommands.Goto:
                            newCommand = ParseGoto(cmdValue.Value as JValue);
                            break;

                        case ScriptCommands.Jump:
                            newCommand = ParseJump(cmdValue.Value as JValue);
                            break;

                        case ScriptCommands.End:
                            newCommand = ParseEnd(cmdValue.Value as JValue);
                            break;

                        default:
                            Debug.Fail(string.Format("Unhandled ScriptCommands value: {0}", cmdValue.Name));
                            break;
                    }
                }
                else
                {
                    if (cmdValue.Name.ToLower() == "forever")
                    {
                        newCommand = ParseForeverCommand(cmdValue.Value as JArray);
                    }
                    else
                    {
                        mStatus.Warning(string.Format("Unknown command \"{0}\".", cmdValue.Name));
                    }
                }

                if (newCommand != null)
                {
                    commands.Add(newCommand);
                }
            }
        }

        private ICommandWriter ParseSetCommand(JObject setValue)
        {
            if (setValue != null)
            {
                Set setCmd = null;
                float time = 0;

                foreach (JProperty property in setValue.Properties())
                {
                    // set only has two properties: target and time
                    // if target has already been seen, this property had better be time
                    if (setCmd != null)
                    {
                        if (property.Name.ToLower() != "time")
                        {
                            mStatus.Warning(string.Format("set contains multiple targets: {0} and {1}. Ignoring {1}.", setCmd.Target, property.Name));
                            continue;
                        }
                    }

                    ScriptTargets target;
                    if (Enum.TryParse<ScriptTargets>(property.Name, true, out target))
                    {
                        switch (target)
                        {
                            case ScriptTargets.Speed:
                                setCmd = new SetFloat(property);
                                break;

                            case ScriptTargets.Acceleration:
                                setCmd = new SetFloat(property);
                                break;

                            case ScriptTargets.MaxSpeed:
                                setCmd = new SetFloat(property);
                                break;

                            case ScriptTargets.PolarVelocity:
                                setCmd = new SetFloatPair(property);
                                break;

                            case ScriptTargets.PolarAcceleration:
                                setCmd = new SetFloatPair(property);
                                break;

                            case ScriptTargets.Velocity_X:
                                setCmd = new SetFloat(property);
                                break;

                            case ScriptTargets.Velocity_Y:
                                setCmd = new SetFloat(property);
                                break;

                            case ScriptTargets.Acceleration_X:
                                setCmd = new SetFloat(property);
                                break;

                            case ScriptTargets.Acceleration_Y:
                                setCmd = new SetFloat(property);
                                break;

                            case ScriptTargets.BoundingRegions:
                                setCmd = new SetBoundingRegions(property);
                                break;

                            case ScriptTargets.Flag:
                                setCmd = new SetInt(property);
                                break;

                            case ScriptTargets.KeyValuePair:
                                setCmd = new SetKeyValuePair(property);
                                break;

                            case ScriptTargets.TextureRect:
                                setCmd = new SetRect(property);
                                break;

                            case ScriptTargets.TextureHotspot:
                                setCmd = new SetHotspot(property);
                                break;

                            case ScriptTargets.TextureFlip:
                                setCmd = new SetFlip(property);
                                break;

                            case ScriptTargets.SpriteOpacity:
                                setCmd = new SetFloat(property);
                                break;

                            default:
                                Debug.Fail(string.Format("Unhandled ScriptTarget value: {0}.", property.Name));
                                break;
                        }
                    }
                    else
                    {
                        if (property.Name.ToLower() == "time")
                        {
                            time = property.Value.Value<uint>();
                        }
                        else
                        {
                            mStatus.Warning(string.Format("Unknown property \"{0}\" in set command", property.Name));
                        }
                    }
                }

                setCmd.Time = time;
                return setCmd;
            }
            else
            {
                mStatus.Error("Set command was not a json dictionary.");
            }

            return null;
        }

        private ICommandWriter ParseIfCommand(JObject ifValue)
        {
            if (ifValue != null)
            {
                If cmd = new If(ifValue["condition"] as JArray);

                ParseCommandBlock(cmd.ThenBlock, ifValue["then"] as JArray);

                JToken elseBlock;
                if (ifValue.TryGetValue("else", out elseBlock))
                {
                    ParseCommandBlock(cmd.ElseBlock, elseBlock as JArray);
                }

                return cmd;
            }
            else
            {
                mStatus.Error("If command was not a json dictionary.");
            }

            return null;
        }

        private ICommandWriter ParseForeverCommand(JArray foreverValue)
        {
            if (foreverValue != null)
            {
                Forever cmd = new Forever();

                ParseCommandBlock(cmd.Commands, foreverValue);

                return cmd;
            }
            else
            {
                mStatus.Error("Forever command was not a json array.");
            }

            return null;
        }

        private ICommandWriter ParseLabel(JValue labelValue)
        {
            if (labelValue != null)
            {
                return new Label(labelValue);
            }
            else
            {
                mStatus.Error("Label command was not a json value.");
            }

            return null;
        }

        private ICommandWriter ParseGoto(JValue gotoValue)
        {
            if (gotoValue != null)
            {
                return new Goto(gotoValue);
            }
            else
            {
                mStatus.Error("Goto command was not a json value.");
            }

            return null;
        }

        private ICommandWriter ParseJump(JValue jumpValue)
        {
            if (jumpValue != null)
            {
                return new Jump(jumpValue);
            }
            else
            {
                mStatus.Error("Jump command was not a json value.");
            }

            return null;
        }

        private ICommandWriter ParseEnd(JValue endValue)
        {
            if (endValue != null)
            {
                return new End();
            }
            else
            {
                mStatus.Error("End command was not a json value.");
            }

            return null;
        }
        #endregion

        public List<byte[]> Compile(IWriterContext context)
        {
            List<byte[]> compiled = new List<byte[]>();

            foreach (var script in Animations)
            {
                context.CurrentScript = script.Name;

                using (MemoryStream scriptStream = new MemoryStream())
                {
                    using (BinaryWriter scriptWriter = new BinaryWriter(scriptStream))
                    {
                        foreach (var command in script.Commands)
                        {
                            command.Write(scriptWriter, context);
                        }
                    }

                    compiled.Add(scriptStream.ToArray());
                }
            }

            return compiled;
        }
    }
}
